/**
 * This file is part of Lineage II Client Data Parser.
 * 
 * Lineage II Client Data Parser is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * Lineage II Client Data Parser is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with Lineage II Client Data Parser.  If not, see <http://www.gnu.org/licenses/>.
 */
package net.l2emuproject.parser;

import java.io.IOException;
import java.io.Writer;
import java.util.ArrayList;
import java.util.List;

import net.l2emuproject.Launcher;
import net.l2emuproject.entity.MonsterSkill;
import net.l2emuproject.entity.MonsterSkillPassive;
import net.l2emuproject.entity.SkillEntry;
import net.l2emuproject.io.FileManager;
import net.l2emuproject.io.SafelyPackedFile;
import net.l2emuproject.parser.base.DefaultParser;
import net.l2emuproject.redist.UnsupportedException;

import javolution.text.TextBuilder;
import javolution.util.FastMap;

/**
 * @author Savormix
 *
 */
public class SkillParser extends DefaultParser
{
	private static final String							NULL						= "";
	// Cannot declare static arrays in lower level types
	private static final String							DEC_SKILL					= "skill";
	private static final String							DEC_SKILL_ATR[]				=
																					{ "id", "levels", "name" };
	private static final String							DEC_SET						= "set";
	private static final String							DEC_SET_ATR[]				=
																					{ "name", "val" };
	private static final String							DEC_EFFECT					= "effect";
	private static final String							DEC_EFFECT_ATR[]			=
																					{ "count", "name", "abnormal", "time", "val", "stackOrder", "stackType" };
	private static final String							DEC_TABLE					= "table";
	private static final String							DEC_TABLE_ATR[]				=
																					{ "name" };
	private static final String							DEC_ENCHANT					= "enchant";
	private static final String							DEC_FOR						= "for";
	private static final String[]						DEC_DESCRIPTION				=
																					{ "Lv", ": " };
	private static final String[]						DEC_DESCRIPTION_ENCHANT		=
																					{ "Enchant: ", " - " };
	private static final int[]							ENCHANT_LEARNING_LEVEL_30	=
																					{
			76,
			76,
			76,
			77,
			77,
			77,
			78,
			78,
			78,
			79,
			79,
			79,
			80,
			80,
			80,
			81,
			81,
			81,
			82,
			82,
			82,
			83,
			83,
			83,
			84,
			84,
			84,
			85,
			85,
			85																		};
	private static final int[]							ENCHANT_LEARNING_LEVEL_15	=
																					{ 81, 81, 81, 82, 82, 82, 83, 83, 83, 84, 84, 84, 85, 85, 85 };
	private static final int[]							DECLARABILITY				=
																					{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 };
	/** Write skill description to XML file? */
	public static int									DESCRIPTION					= 1;
	/** Write NPC list to XML file? */
	public static boolean								DESCRIPTION_NPC				= false;
	/** Write each skill type as NOTDONE? */
	public static boolean								NOTDONE						= false;
	/** Write a reuse delay equal to cast time? */
	public static boolean								REUSE						= false;
	protected static final String[]						NEEDED_FILES				=
																					{ Launcher.FILES[32], Launcher.FILES[31] };
	private final FastMap<Integer, List<SkillEntry>>	skills;
	private List<MonsterSkill>							expMA						= null;
	private List<MonsterSkillPassive>					expMP						= null;

	/**
	 * Creates a parser that parses skill data. Needed Files:
	 * <LI>skillname-e</LI>
	 * <LI>Skillgrp</LI>
	 * @param spf File array
	 * @throws IllegalArgumentException Wrong file(s)
	 */
	public SkillParser(SafelyPackedFile... spf) throws IllegalArgumentException
	{
		super(spf);
		skills = FastMap.newInstance();
		skills.shared();
		if (entries[order[0]] != entries[order[1]])
			log.warn("Incompatible name and grp files: either might be corrupt!");
	}

	/** Creates a parser that validates files */
	public SkillParser()
	{
		super();
		skills = null;
	}

	private boolean parseSkillName(int entryCnt, SafelyPackedFile spf)
	{
		int id, level;
		List<SkillEntry> list;
		if (!isValidator())
			log.info("Reading " + entryCnt + " entries from " + spf.getFileName());
		for (int i = 0; i < entryCnt; i++)
		{
			id = spf.readInt();
			level = spf.readInt();
			if (isValidator())
			{
				log.debug("Validating skill name. Entries: " + entryCnt + "; 1st id = " + id + " level: " + level);
				return (id > 0 && level < 1000);
			}
			if ((list = skills.get(id)) == null)
			{
				List<SkillEntry> tmp = new ArrayList<SkillEntry>();
				skills.putIfAbsent(id, tmp);
				list = skills.get(id);
			}
			log.debug("Reading skill: " + id + ", level: " + level);
			if (id < 0 || id > 50000)
				log.debug("WAIT!");
			String n = spf.readString(), d = spf.readString(), en = spf.readString(), ed = spf.readString();
			log.debug("Name: " + n);
			log.debug("Desc: " + d);
			log.debug("EnchName: " + en);
			log.debug("EnchDesc: " + ed);
			list.add(new SkillEntry(id, level, n, d, en, ed));
		}
		log.info("Finished reading " + spf.getFileName());
		return false;
	}

	private boolean parseSkillGrp(int entryCnt, SafelyPackedFile spf)
	{
		List<SkillEntry> list;
		int id, level;
		if (!isValidator())
			log.info("Reading " + entryCnt + " entries from " + spf.getFileName());
		for (int i = 0; i < entryCnt; i++)
		{
			id = spf.readInt();
			level = spf.readInt();
			if (isValidator())
				return (id > 0 && level < 1000);
			log.debug("Reading skill id: " + id + " level " + level);
			list = skills.get(id);
			if (list == null)
			{
				log.warn("Grp contains skill " + id + ";" + level + " that is not included in name!");
				readIgnoredSkillEntry(spf);
				continue;
			}
			SkillEntry se = null;
			for (SkillEntry listed : list)
				if (listed.getLevel() == level)
					se = listed;
			if (se == null)
			{
				log.warn("Grp contains skill " + id + ";" + level + " that is not included in name!");
				readIgnoredSkillEntry(spf);
				continue;
			}
			se.setOperateType(spf.readInt());
			if (spf.getChronicleID() > 7)
				se.setUnk_0(spf.readInt());
			else
				se.setUnk_0(0);
			se.setMana(spf.readInt());
			se.setRange(spf.readInt());
			se.setStyle(spf.readInt());
			se.setTime(spf.readFloat());
			se.setMagic(spf.readInt() != 0);
			se.setAnimationChar(spf.readPreciseString(spf.readInt()));
			se.setDesc(spf.readPreciseString(spf.readInt()));
			se.setIcon1(spf.readPreciseString(spf.readInt()));
			se.setIcon2(spf.readPreciseString(spf.readInt()));
			se.setEnchanted(spf.readInt() != 0);
			se.setEnchSID(spf.readInt());
			// WTF is this value? Seems to be >= 5 for enchanted levels, 0 for plain
			se.setUnk_1(spf.readInt());
			se.setHealth(spf.readInt());
			se.setUnk_2(spf.readInt());
			se.setUnk_3(spf.readInt());
		}
		log.info("Finished reading " + spf.getFileName());
		return false;
	}

	private final void readIgnoredSkillEntry(SafelyPackedFile spf)
	{
		spf.readInt();
		if (spf.getChronicleID() > 7)
			spf.readInt();
		spf.readInt();
		spf.readInt();
		spf.readInt();
		spf.readFloat();
		spf.readInt();
		spf.readString(spf.readInt());
		spf.readString(spf.readInt());
		spf.readUTF16();
		spf.readUTF16();
		spf.readInt();
		spf.readInt();
		spf.readInt();
		spf.readInt();
		spf.readInt();
		spf.readInt();
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#getNeededFiles()
	 */
	@Override
	public String[] getNeededFiles()
	{
		return NEEDED_FILES;
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#getParseType()
	 */
	@Override
	public String getParseType()
	{
		return "skill";
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#readClientData()
	 */
	@Override
	public void readClientData() throws IllegalAccessException
	{
		if (isValidator())
			throw new IllegalAccessException(toString());
		parseSkillName(entries[order[0]], spf[order[0]]);
		parseSkillGrp(entries[order[1]], spf[order[1]]);
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#selfAdvancedAnalysis()
	 */
	@Override
	public void selfAdvancedAnalysis() throws IllegalAccessException
	{
		if (isValidator())
			throw new IllegalAccessException(toString());
		for (List<SkillEntry> list : skills.values())
			for (SkillEntry se : list)
			{
				if (ANALYZE_DETERMINISTIC)
					se.deterministicAnalysis();
				if (ANALYZE_EXPERIMENTAL)
					se.experimentalAnalysis();
			}
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#validate(com.l2cdp.entity.SafelyPackedFile)
	 */
	@Override
	public boolean validate(SafelyPackedFile spf)
	{
		try
		{
			if (spf.getFileName().equalsIgnoreCase(NEEDED_FILES[0]))
				return parseSkillName(spf.readInt(), spf);
			else if (spf.getFileName().equalsIgnoreCase(NEEDED_FILES[1]))
				return parseSkillGrp(spf.readInt(), spf);
			else
				return false;
		}
		catch (Exception e)
		{
			return false;
		}
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#writeServerData()
	 */
	@Override
	public void writeServerData(int chronicle) throws IllegalAccessException
	{
		if (isValidator())
			throw new IllegalAccessException(toString());
		if (ANALYZE_EXPERIMENTAL)
		{
			try
			{
				MonsterSkillParser msp = new MonsterSkillParser(FileManager.getSPFile(chronicle, MonsterSkillParser.NEEDED_FILES));
				msp.readClientData();
				msp.selfAdvancedAnalysis();
				expMA = msp.getAltSkillList();
				expMP = msp.getSkillList();
			}
			catch (IOException e)
			{
				log.warn("Failure during experimental analysis!", e);
			}
			catch (UnsupportedException e)
			{
				log.warn("Failure during experimental analysis!", e);
			}
		}
		synchronized (finLock)
		{
			for (int i = 0; i < AsyncWriterSkSrv.THREAD_COUNT; i++)
				addThread();
		}
		for (int i = 0; i < AsyncWriterSkSrv.THREAD_COUNT; i++)
			new Thread(new AsyncWriterSkSrv(chronicle, i * AsyncWriterSkSrv.SKILLS_PER_FILE * AsyncWriterSkSrv.FILES_PER_THREAD),
					AsyncWriterSkSrv.class.getName() + "-" + i).start();
	}

	/* (non-Javadoc)
	 * @see com.l2cdp.parser.IParser#writeSimpleData()
	 */
	@Override
	public void writeSimpleData(int chronicle) throws IllegalAccessException
	{
		if (isValidator())
			throw new IllegalAccessException(toString());
		synchronized (finLock)
		{
			addThread();
		}
		new Thread(new AsyncWriterSkSim(chronicle), AsyncWriterSkSim.class.getName()).start();
	}

	protected class AsyncWriterSkSim implements Runnable
	{
		private final int	cID;

		protected AsyncWriterSkSim(int cID)
		{
			this.cID = cID;
		}

		/* (non-Javadoc)
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run()
		{
			TextBuilder tb = TextBuilder.newInstance();
			try
			{
				Writer w = FileManager.getWriter(cID, "skill_desc.properties", true);
				addGenInfo(cID, tb, COMMENT_PROP);
				for (List<SkillEntry> skill : skills.values())
				{
					for (SkillEntry se : skill)
					{
						tb.append(se.getId());
						tb.append(EQUAL);
						tb.append(se.getDescS());
						tb.append('|');
						tb.append(se.getDescE());
						tb.append(LINE_BREAK);
					}
				}
				addGenInfo(-1, tb, COMMENT_PROP);
				w.write(tb.toString());
				w.close();
			}
			catch (UnsupportedException e)
			{
				log.fatal("Unsupported chronicle, will not write data!", e);
			}
			catch (IOException e)
			{
				log.error("Cannot write skill_desc.properties!", e);
			}
			finally
			{
				TextBuilder.recycle(tb);
				synchronized (finLock)
				{
					remThread();
				}
			}
		}
	}

	// Set/enchantX attributes
	// They should have been stored in AsyncWriterSkSrv,
	// but inner classes can't contain static arrays OR references to static arrays
	private static final String[]	DECLARABLE				=
															{
			"magicLvl",
			"weaponsAllowed",
			"power",
			"baseCritRate",
			"lvlDepend",
			"SSBoost",
			"giveCharges",
			"maxCharges",
			"",
			"needCharges",
			"mpInitialConsume",
			"mpConsume",
			"nextDanceCost",
			"hpConsume",
			"element",
			"elementPower",
			"overHit",
			"ignoreShld",
			"isMagic",
			"isDebuff",
			"isPotion",
			"isDance",
			"isSong",
			"reuseDelay",
			"staticReuse",
			"hitTime",
			"staticHitTime",
			"coolTime",
			"target",
			"skillType",
			"skillRadius",
			"operateType",
			"castRange",
			"effectRange"									};
	private static final String		DEC_LEARN_LEVEL			= DECLARABLE[0];
	private static final String		DEC_WEAPON_TYPE			= DECLARABLE[1];
	private static final String		DEC_POWER				= DECLARABLE[2];
	private static final String		DEC_CRITICAL			= DECLARABLE[3];
	private static final String		DEC_LEVEL_DEPENDENCY	= DECLARABLE[4];
	private static final String		DEC_SHOT_BOOST			= DECLARABLE[5];
	private static final String		DEC_CHARGES_GIVEN		= DECLARABLE[6];
	private static final String		DEC_CHARGES_MAX			= DECLARABLE[7];
	@SuppressWarnings("unused")
	private static final String		DEC_CHARGES_TYPE		= DECLARABLE[8];	// unnecessary, not implemented
	@SuppressWarnings("unused")
	private static final String		DEC_CHARGES_CONSUMED	= DECLARABLE[9];
	private static final String		DEC_MP_BEFORE			= DECLARABLE[10];
	private static final String		DEC_MP_AFTER			= DECLARABLE[11];
	private static final String		DEC_MP_CONT_DANCE		= DECLARABLE[12];
	private static final String		DEC_HP_AFTER			= DECLARABLE[13];
	private static final String		DEC_ELEMENT_TYPE		= DECLARABLE[14];
	private static final String		DEC_ELEMENT_POWER		= DECLARABLE[15];
	private static final String		DEC_OVERHIT				= DECLARABLE[16];
	private static final String		DEC_IGNORE_S			= DECLARABLE[17];
	private static final String		DEC_MAGIC				= DECLARABLE[18];
	private static final String		DEC_DEBUFF				= DECLARABLE[19];
	@SuppressWarnings("unused")
	private static final String		DEC_POTION				= DECLARABLE[20];
	private static final String		DEC_DANCE				= DECLARABLE[21];
	private static final String		DEC_SONG				= DECLARABLE[22];
	private static final String		DEC_TIME_REUSE			= DECLARABLE[23];
	private static final String		DEC_STATIC_REUSE		= DECLARABLE[24];
	private static final String		DEC_TIME_HIT			= DECLARABLE[25];
	@SuppressWarnings("unused")
	private static final String		DEC_STATIC_HIT_TIME		= DECLARABLE[26];
	private static final String		DEC_TIME_COOL			= DECLARABLE[27];
	private static final String		DEC_TARGET_TYPE			= DECLARABLE[28];
	private static final String		DEC_SKILL_TYPE			= DECLARABLE[29];
	private static final String		DEC_SKILL_RADIUS		= DECLARABLE[30];
	private static final String		DEC_OPERATE_TYPE		= DECLARABLE[31];
	private static final String		DEC_RANGE_CAST			= DECLARABLE[32];
	private static final String		DEC_RANGE_EFFECT		= DECLARABLE[33];
	private static final String[]	DECLARE_ALWAYS			= {
															//DEC_POWER, DEC_TIME_REUSE
															};

	protected class AsyncWriterSkSrv implements Runnable
	{
		private static final int	FILES_PER_THREAD	= 80;
		private static final int	SKILLS_PER_FILE		= 50;
		private static final int	NUM_LENGTH			= 5;
		private static final int	THREAD_COUNT		= 10;
		private static final String	FILETYPE			= ".xml";
		private static final String	SKILLS				= "skills";
		private static final String	LIST				= "list";
		private static final String	NPC_LIST			= "Used by NPCs:";
		private static final String	DEC_E_LEVELS		= "enchantLevels";
		private static final String	LEARN_LEVEL_TABLE	= "#enchantMagicLvl";
		private static final String	NOT_ENCHANTED		= "none";
		private static final String	FALSE				= "false";

		private final int			cID;
		private final int			first;										// inclusive

		protected AsyncWriterSkSrv(int cID, int first)
		{
			this.cID = cID;
			this.first = first;
		}

		protected final String generateFileName(int first, int last)
		{
			TextBuilder tb = TextBuilder.newInstance();
			String s = String.valueOf(first);
			for (int i = s.length(); i < NUM_LENGTH; i++)
				tb.append(0);
			tb.append(s);
			tb.append('-');
			s = String.valueOf(last - 1);
			for (int i = s.length(); i < NUM_LENGTH; i++)
				tb.append(0);
			tb.append(s);
			tb.append(FILETYPE);
			String res = tb.toString();
			TextBuilder.recycle(tb);
			return res;
		}

		protected final int[] getLevelCount(List<SkillEntry> skill)
		{
			int[] result = new int[DECLARABILITY.length];
			System.arraycopy(DECLARABILITY, 0, result, 0, result.length);
			for (SkillEntry se : skill)
				result[se.getLevel() / 100]++;
			return result;
		}

		protected final String getPower(int skillPower)
		{
			return String.valueOf(skillPower);
		}

		private final String getTableName(String atr, int enchant)
		{
			TextBuilder tb = TextBuilder.newInstance();
			tb.append('#');
			if (enchant > 0)
			{
				tb.append(DEC_ENCHANT);
				tb.append(enchant);
			}
			tb.append(atr);
			String s = tb.toString();
			TextBuilder.recycle(tb);
			return s;
		}

		private final String getEnchSet(int level)
		{
			TextBuilder tb = TextBuilder.newInstance();
			tb.append(DEC_ENCHANT);
			tb.append(level);
			String s = tb.toString();
			TextBuilder.recycle(tb);
			return s;
		}

		protected final void xmlDeclareSet(TextBuilder tb, String name, Object value)
		{
			xmlDeclare(tb, DEC_SET, DEC_SET_ATR, new Object[]
			{ name, value }, true, 2);
		}

		protected final void xmlDeclareSet(TextBuilder tb, int enchant, String name, Object value)
		{
			if (enchant > 0)
				xmlDeclare(tb, getEnchSet(enchant), DEC_SET_ATR, new Object[]
				{ name, value }, true, 2);
			else
				xmlDeclareSet(tb, name, value);
		}

		protected final void xmlDeclareTable(TextBuilder tb, int enchant, String name, Object[] values)
		{
			if (useTable(values))
			{
				String tn = getTableName(name, enchant);
				xmlDeclare(tb, DEC_TABLE, DEC_TABLE_ATR, new Object[]
				{ tn }, false, 2);
				tb.delete(tb.length() - LINE_BREAK.length(), tb.length());
				for (Object o : values)
				{
					tb.append(SPACE);
					tb.append(o);
				}
				tb.append(SPACE);
				xmlDeclareFinish(tb, DEC_TABLE, 0);
				xmlDeclareSet(tb, enchant, name, tn);
			}
			else
				xmlDeclareSet(tb, enchant, name, values[0]);
		}

		protected final void xmlDeclareEnchantLL(TextBuilder tb, boolean special)
		{
			xmlDeclare(tb, DEC_TABLE, DEC_TABLE_ATR, new Object[]
			{ LEARN_LEVEL_TABLE }, false, 2);
			tb.delete(tb.length() - LINE_BREAK.length(), tb.length());
			if (special)
			{
				for (int i : ENCHANT_LEARNING_LEVEL_15)
				{
					tb.append(SPACE);
					tb.append(i);
				}
			}
			else
			{
				for (int i : ENCHANT_LEARNING_LEVEL_30)
				{
					tb.append(SPACE);
					tb.append(i);
				}
			}
			tb.append(SPACE);
			xmlDeclareFinish(tb, DEC_TABLE, 0);
		}

		private final boolean useTable(Object[] values)
		{
			for (int i = 1; i < values.length; i++)
				// must transform to string, or 512 != 512; 524 != 524 etc.
				if (!values[i].toString().equals(values[i - 1].toString()))
					return true;
			return false;
		}

		private final void xmlDeclareFor(TextBuilder tb, int enchant, boolean finish)
		{
			if (finish)
			{
				if (enchant > 0)
					xmlDeclareFinish(tb, getEnchSet(enchant) + DEC_FOR, 2);
				else
					xmlDeclareFinish(tb, DEC_FOR, 2);
			}
			else
			{
				if (enchant > 0)
					xmlDeclare(tb, getEnchSet(enchant) + DEC_FOR, false, 2);
				else
					xmlDeclare(tb, DEC_FOR, false, 2);
			}
		}

		private final void xmlDeclareEffect(TextBuilder tb, int count, String name, String abnormal, int time, float val, float stackOrder, String stackType)
		{
			xmlDeclare(tb, DEC_EFFECT, DEC_EFFECT_ATR, new Object[]
			{ count, name, abnormal, time, val, stackOrder, stackType }, false, 3);
		}

		private final void xmlDeclareEffect(TextBuilder tb)
		{
			xmlDeclareFinish(tb, DEC_EFFECT, 3);
		}

		/**
		 * Used <U>only</U> for multiple-level skills.
		 * @param npcID NPC's ID
		 * @param level Skill level
		 * @return
		 */
		protected final String getNpcComment(int npcID, int level)
		{
			TextBuilder tb = TextBuilder.newInstance();
			tb.append(npcID);
			tb.append(DEC_DESCRIPTION_ENCHANT[1]);
			tb.append(DEC_DESCRIPTION[0]);
			tb.append(level);
			String s = tb.toString();
			TextBuilder.recycle(tb);
			return s;
		}

		private final void populateKnown(FastMap<String, FastMap<Integer, Object>> reusableMap)
		{
			if (reusableMap.isEmpty())
				for (String key : DECLARABLE)
				{
					FastMap<Integer, Object> map = FastMap.newInstance();
					reusableMap.put(key, map);
				}
			else
				for (String key : reusableMap.keySet())
					reusableMap.get(key).clear();
		}

		private final boolean declareIt(Object[] values, Object unenchanted, int enchant) throws IllegalArgumentException
		{
			boolean declarable = false;
			if (values == null || values.length == 0)
				return declarable;
			if (values instanceof Boolean[])
				declarable = declareIt((Boolean[]) values);
			else if (values instanceof Float[])
				declarable = declareIt((Float[]) values);
			else if (values instanceof Integer[])
				declarable = declareIt((Integer[]) values);
			else if (values instanceof String[])
				declarable = declareIt((String[]) values);
			else
			{
				try
				{
					Float[] val = new Float[values.length];
					for (int i = 0; i < values.length; i++)
						val[i] = Float.parseFloat(String.valueOf(values[i]));
					declarable = declareIt(val);
				}
				catch (NumberFormatException e)
				{
					String[] val = new String[values.length];
					for (int i = 0; i < values.length; i++)
						val[i] = String.valueOf(values[i]);
					declarable = declareIt(val);
				}
			}
			if (enchant > 0)
				return declarable && !values[values.length - 1].toString().equals(unenchanted.toString());
			else
				return declarable;
		}

		private final boolean declareIt(Boolean[] values)
		{
			for (int i = 0; i < values.length; i++)
				if (values[i])
					return true;
			return false;
		}

		private final boolean declareIt(Integer[] values)
		{
			for (int i = 0; i < values.length; i++)
				if (values[i] != 0)
					return true;
			return false;
		}

		private final boolean declareIt(Float[] values)
		{
			for (int i = 0; i < values.length; i++)
				if (values[i] != 0)
					return true;
			return false;
		}

		private final boolean declareIt(String[] values)
		{
			for (int i = 0; i < values.length; i++)
				if (!values[i].isEmpty() && !values[i].equals(FALSE))
					return true;
			return false;
		}

		private final boolean declareAlways(String attr)
		{
			for (String s : DECLARE_ALWAYS)
				if (s.equals(attr))
					return true;
			return false;
		}

		/* (non-Javadoc)
		 * @see java.lang.Runnable#run()
		 */
		@Override
		public void run()
		{
			try
			{
				int firstSkill, lastSkill;
				for (int i = 0; i < FILES_PER_THREAD; i++)
				{
					firstSkill = first + i * SKILLS_PER_FILE;
					lastSkill = firstSkill + SKILLS_PER_FILE;
					Writer w = FileManager.getWriter(cID, generateFileName(firstSkill, lastSkill), false);
					TextBuilder tb = TextBuilder.newInstance();
					xmlDeclare(tb);
					xmlDeclareDoc(tb, SKILLS);
					addGenInfo(cID, tb, COMMENT_XML);
					xmlDeclare(tb, LIST, false, 0);

					FastMap<String, FastMap<Integer, Object>> multiLevel = FastMap.newInstance();
					multiLevel.shared();
					List<SkillEntry> levels;
					SkillEntry se;
					boolean haSkill = false;
					for (int j = firstSkill; j < lastSkill; j++)
					{
						levels = skills.get(j);
						if (levels == null || levels.size() == 0)
							continue;
						haSkill = true;
						int[] lvls = getLevelCount(levels);
						if (levels.size() == 1)
						{
							se = levels.get(0);
							if (DESCRIPTION > 0 && (DESCRIPTION == 3 || !se.getDescS().isEmpty()))
								xmlComment(tb, se.getDescS(), 1);
							if (DESCRIPTION_NPC && expMA != null && expMP != null)
							{
								List<String> npcs = new ArrayList<String>();
								for (MonsterSkillPassive npcSkill : expMP)
									if (npcSkill.getSkillID() == j)
										npcs.add(String.valueOf(npcSkill.getNpcID()));
								for (MonsterSkill npcSkill : expMA)
									if (npcSkill.getSkillID() == j && !npcs.contains(String.valueOf(npcSkill.getNpcID())))
										npcs.add(String.valueOf(npcSkill.getNpcID()));
								if (!npcs.isEmpty())
								{
									String[] npcList = new String[npcs.size() + 1];
									npcList[0] = NPC_LIST;
									while (!npcs.isEmpty())
										npcList[npcs.size()] = npcs.remove(npcs.size() - 1);
									xmlComment(tb, npcList, 1);
								}
							}
						}
						else
						{
							String d, ld = NULL;
							String en, ed, led = NULL;
							if (DESCRIPTION > 0)
							{
								List<String> lines = new ArrayList<String>();
								TextBuilder line = TextBuilder.newInstance();
								for (SkillEntry level : levels)
								{
									d = level.getDescS();
									en = level.getNameE();
									ed = level.getDescE();
									if ((DESCRIPTION >= 2 || !ld.equals(d)) && (DESCRIPTION == 3 || !d.isEmpty()))
									{
										line.append(DEC_DESCRIPTION[0]);
										line.append(level.getLevel());
										line.append(DEC_DESCRIPTION[1]);
										line.append(d);
										lines.add(line.toString());
										line.clear();
									}
									ld = d;
									if ((DESCRIPTION == 3 || !en.equals(NOT_ENCHANTED)) && (DESCRIPTION >= 2 || !led.equals(ed)))
									{
										line.append(DEC_DESCRIPTION_ENCHANT[0]);
										line.append(en);
										line.append(DEC_DESCRIPTION_ENCHANT[1]);
										line.append(ed);
										lines.add(line.toString());
										line.clear();
									}
									led = ed;
								}
								if (!lines.isEmpty())
									xmlComment(tb, lines.toArray(new String[lines.size()]), 1);
								lines.clear();
							}
							if (DESCRIPTION_NPC && expMA != null && expMP != null)
							{
								List<String> npcs = new ArrayList<String>();
								for (MonsterSkillPassive npcSkill : expMP)
									if (npcSkill.getSkillID() == j)
										npcs.add(getNpcComment(npcSkill.getNpcID(), npcSkill.getSkillLevel()));
								for (MonsterSkill npcSkill : expMA)
									if (npcSkill.getSkillID() == j && !npcs.contains(getNpcComment(npcSkill.getNpcID(), npcSkill.getSkillLevel())))
										npcs.add(getNpcComment(npcSkill.getNpcID(), npcSkill.getSkillLevel()));
								if (!npcs.isEmpty())
								{
									String[] npcList = new String[npcs.size() + 1];
									npcList[0] = NPC_LIST;
									while (!npcs.isEmpty())
										npcList[npcs.size()] = npcs.remove(npcs.size() - 1);
									xmlComment(tb, npcList, 1);
								}
							}
						}
						List<String> attribs = new ArrayList<String>(DEC_SKILL_ATR.length);
						List<Object> values = new ArrayList<Object>(DEC_SKILL_ATR.length);
						for (String s : DEC_SKILL_ATR)
							attribs.add(s);
						values.add(j);
						values.add(lvls[0]);
						values.add(levels.get(levels.size() - 1).getNameS());
						for (int k = 1; k < lvls.length; k++)
						{
							if (lvls[k] == 0)
								continue;
							attribs.add(DEC_E_LEVELS + k);
							values.add(lvls[k]);
						}
						xmlDeclare(tb, DEC_SKILL, attribs.toArray(new String[attribs.size()]), values.toArray(), false, 1);
						attribs.clear();
						values.clear();
						if (levels.size() == 1)
						{
							se = levels.get(0);
							if (se.getWeaponToCast() > 0)
								xmlDeclareSet(tb, DEC_WEAPON_TYPE, se.getWeaponToCast());
							if (se.getPower() > 0)
								xmlDeclareSet(tb, DEC_POWER, getPower(se.getPower()));
							if (se.getCritRate() > 0)
								xmlDeclareSet(tb, DEC_CRITICAL, se.getCritRate());
							if (se.getDependency() > 0)
								xmlDeclareSet(tb, DEC_LEVEL_DEPENDENCY, se.getDependency());
							if (se.getSkillType().equals(SkillEntry.SKILL_DMG_PHYSIC))
								xmlDeclareSet(tb, DEC_SHOT_BOOST, getPower(2));
							if (se.getCharges() > 0)
							{
								xmlDeclareSet(tb, DEC_CHARGES_GIVEN, se.getChargesGiven());
								xmlDeclareSet(tb, DEC_CHARGES_MAX, se.getCharges());
							}
							// TODO: charge consume count
							// TODO: max soul consume count
							if (se.getMana() > 0)
							{
								if (!se.isToggle())
								{
									int mpi = Math.round(se.getMana() * 0.2F);
									if (se.isMagic() && mpi > 0)
									{
										xmlDeclareSet(tb, DEC_MP_BEFORE, mpi);
										xmlDeclareSet(tb, DEC_MP_AFTER, (se.getMana() - mpi));
									}
									else
										xmlDeclareSet(tb, DEC_MP_AFTER, se.getMana());
								}
								else
									xmlDeclareSet(tb, DEC_MP_BEFORE, se.getMana());
								if (se.isDance() || se.isSong())
									xmlDeclareSet(tb, DEC_MP_CONT_DANCE, 30/*se.getMana() / 2*/);
							}
							if (se.getHealth() > 0)
								xmlDeclareSet(tb, DEC_HP_AFTER, se.getHealth());
							if (se.getElementType() > 0)
							{
								xmlDeclareSet(tb, DEC_ELEMENT_TYPE, se.getElementType());
								xmlDeclareSet(tb, DEC_ELEMENT_POWER, se.getElementPower());
							}
							if (se.isOverhit())
								xmlDeclareSet(tb, DEC_OVERHIT, true);
							if (se.ignoresShield())
								xmlDeclareSet(tb, DEC_IGNORE_S, true);
							if (se.isMagic())
								xmlDeclareSet(tb, DEC_MAGIC, true);
							if (se.isDebuff())
								xmlDeclareSet(tb, DEC_DEBUFF, true);
							// XXX: isPotion??!
							if (se.isDance())
								xmlDeclareSet(tb, DEC_DANCE, true);
							else if (se.isSong())
								xmlDeclareSet(tb, DEC_SONG, true);
							if (REUSE && !se.isPassive())
								//xmlDeclareSet(tb, DEC_TIME_REUSE, SkillEntry.DEFAULT_REUSE);
								xmlDeclareSet(tb, DEC_TIME_REUSE, se.getHitTime());
							if (se.isStaticReuse())
								xmlDeclareSet(tb, DEC_STATIC_REUSE, true);
							if (se.getTime() > 0)
							{
								xmlDeclareSet(tb, DEC_TIME_HIT, se.getHitTime());
								// TODO: static hitTime
								if (se.getCoolTime() > 0)
									xmlDeclareSet(tb, DEC_TIME_COOL, se.getCoolTime());
							}
							xmlDeclareSet(tb, DEC_TARGET_TYPE, se.getTargetType());
							if (NOTDONE && !se.isRaidDesc())
								xmlDeclareSet(tb, DEC_SKILL_TYPE, SkillEntry.SKILL_NOTDONE);
							else
								xmlDeclareSet(tb, DEC_SKILL_TYPE, se.getSkillType());
							if (se.getRadius() > 0)
								xmlDeclareSet(tb, DEC_SKILL_RADIUS, se.getRadius());
							xmlDeclareSet(tb, DEC_OPERATE_TYPE, se.getOpType());
							if (se.getCastRange() > 0)
								xmlDeclareSet(tb, DEC_RANGE_CAST, se.getCastRange());
							if (se.getEffectRange() > 0)
								xmlDeclareSet(tb, DEC_RANGE_EFFECT, se.getEffectRange());
							if (!se.getEffectType().isEmpty())
							{
								xmlDeclareFor(tb, 0, false);
								xmlDeclareFor(tb, 0, true);
								xmlDeclareEffect(tb, se.getEffectCount(), se.getEffectType(), se.getEffectState(), se.getEffectLength(), se.getEffectDmg(),
										se.getEffectLevel(), se.getEffectStackType());
								xmlDeclareEffect(tb);
							}
						}
						else
						{
							// TODO: optimize, reduce the loop count
							// I had this idea to Map everything:
							// Map<String, Map<Integer, Object>>
							// where String - DEC_TYPE, the Integer - skill level,
							// the Object - value of that DEC_TYPE
							if (lvls[1] > 0)
							{
								// skill is enchantable
								xmlDeclareEnchantLL(tb, lvls[1] == 15);
								for (int k = 1; k < lvls.length; k++)
									if (lvls[k] > 0)
										xmlDeclareSet(tb, k, DEC_LEARN_LEVEL, LEARN_LEVEL_TABLE);
							}
							populateKnown(multiLevel);
							for (SkillEntry level : levels)
							{
								multiLevel.get(DEC_WEAPON_TYPE).put(level.getLevel(), level.getWeaponToCast());
								multiLevel.get(DEC_POWER).put(level.getLevel(), level.getPower());
								multiLevel.get(DEC_CRITICAL).put(level.getLevel(), level.getCritRate());
								multiLevel.get(DEC_LEVEL_DEPENDENCY).put(level.getLevel(), level.getDependency());
								multiLevel.get(DEC_SHOT_BOOST)
										.put(level.getLevel(), level.getSkillType().equals(SkillEntry.SKILL_DMG_PHYSIC) ? getPower(2) : 0);
								multiLevel.get(DEC_CHARGES_GIVEN).put(level.getLevel(), level.getChargesGiven());
								multiLevel.get(DEC_CHARGES_MAX).put(level.getLevel(), level.getCharges());
								if (!level.isToggle())
								{
									int mpi = Math.round(level.getMana() * 0.2F);
									if (level.isMagic() && mpi > 0)
									{
										multiLevel.get(DEC_MP_BEFORE).put(level.getLevel(), mpi);
										multiLevel.get(DEC_MP_AFTER).put(level.getLevel(), (level.getMana() - mpi));
									}
									else
										multiLevel.get(DEC_MP_AFTER).put(level.getLevel(), level.getMana());
								}
								else
									multiLevel.get(DEC_MP_BEFORE).put(level.getLevel(), level.getMana());
								multiLevel.get(DEC_MP_CONT_DANCE).put(level.getLevel(), (level.isDance() || level.isSong()) ? 30 : 0);
								multiLevel.get(DEC_HP_AFTER).put(level.getLevel(), level.getHealth());
								multiLevel.get(DEC_ELEMENT_TYPE).put(level.getLevel(), level.getElementType());
								multiLevel.get(DEC_ELEMENT_POWER).put(level.getLevel(), level.getElementPower());
								multiLevel.get(DEC_OVERHIT).put(level.getLevel(), level.isOverhit());
								multiLevel.get(DEC_IGNORE_S).put(level.getLevel(), level.ignoresShield());
								multiLevel.get(DEC_MAGIC).put(level.getLevel(), level.isMagic());
								multiLevel.get(DEC_DEBUFF).put(level.getLevel(), level.isDebuff());
								multiLevel.get(DEC_DANCE).put(level.getLevel(), level.isDance());
								if (!REUSE || level.isPassive())
									multiLevel.get(DEC_TIME_REUSE).put(level.getLevel(), 0);
								else
									//multiLevel.get(DEC_TIME_REUSE).put(level.getLevel(), SkillEntry.DEFAULT_REUSE);
									multiLevel.get(DEC_TIME_REUSE).put(level.getLevel(), level.getHitTime());
								multiLevel.get(DEC_STATIC_REUSE).put(level.getLevel(), level.isStaticReuse());
								multiLevel.get(DEC_TIME_HIT).put(level.getLevel(), level.getHitTime());
								//multiLevel.get(DEC_STATIC_HIT_TIME).put(level.getLevel(), level.isStaticTime());
								//multiLevel.get(DEC_TIME_COOL).put(level.getLevel(), level.getCoolTime());
								multiLevel.get(DEC_TARGET_TYPE).put(level.getLevel(), level.getTargetType());
								if (NOTDONE && !level.isRaidDesc())
									multiLevel.get(DEC_SKILL_TYPE).put(level.getLevel(), SkillEntry.SKILL_NOTDONE);
								else
									multiLevel.get(DEC_SKILL_TYPE).put(level.getLevel(), level.getSkillType());
								multiLevel.get(DEC_SKILL_TYPE).put(level.getLevel(), level.getSkillType());
								multiLevel.get(DEC_SKILL_RADIUS).put(level.getLevel(), level.getRadius());
								multiLevel.get(DEC_OPERATE_TYPE).put(level.getLevel(), level.getOpType());
								multiLevel.get(DEC_RANGE_CAST).put(level.getLevel(), level.getCastRange());
								multiLevel.get(DEC_RANGE_EFFECT).put(level.getLevel(), level.getEffectRange());
							}
							List<Object> enchVals = new ArrayList<Object>(30);
							Object last = null;
							int lastLvl = 1;
							for (String element : multiLevel.keySet())
							{
								FastMap<Integer, Object> all = multiLevel.get(element);
								for (Integer lvl : all.keySet())
								{
									// Last unenchanted level value
									if (lvl / 100 == 0)
										last = all.get(lvl);
									// wtfEmpty? Well, no need to clear...
									if (lvl % 100 == 1 && !enchVals.isEmpty())
									{
										Object[] vals = enchVals.toArray();
										if (declareIt(vals, last, lvl / 100 - 1) || (declareAlways(element) && (lvl / 100 - 1 == 0)))
											xmlDeclareTable(tb, lvl / 100 - 1, element, vals);
										enchVals.clear();
									}
									enchVals.add(all.remove(lvl));
									lastLvl = lvl;
								}
								Object[] vals = enchVals.toArray();
								if (declareIt(vals, last, lastLvl / 100))
									xmlDeclareTable(tb, lastLvl / 100, element, vals);
								enchVals.clear();
							}
						}
						xmlDeclareFinish(tb, DEC_SKILL, 1);
					}
					xmlDeclareFinish(tb, LIST, 0);
					addGenInfo(-1, tb, COMMENT_XML);
					w.write(tb.toString());
					w.close();
					if (!haSkill)
						FileManager.deleteEmptyFile(cID, generateFileName(firstSkill, lastSkill), false);
				}
			}
			catch (UnsupportedException e)
			{
				log.fatal("Unsupported chronicle, will not write data!", e);
			}
			catch (IOException e)
			{
				log.error("Cannot write skill XML!", e);
			}
			finally
			{
				synchronized (finLock)
				{
					remThread();
				}
			}
		}
	}
}
